package com.inetools.gwetons.client.cssStyleChangerBySelector;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.safehtml.shared.SimpleHtmlSanitizer;
import com.inetools.gwetons.client.JavaScriptObjectUtil;
import com.inetools.gwetons.client.RegExpJsni;
import com.inetools.gwetons.client.logging.JsniLogUtilAdapterTemplate;
import com.inetools.gwetons.shared.logging.LogUtil;

/**
 * CssStyleChangerDataSet class represents top level cscbsDataSet JavaScript object
 * @author ay
 */
class CssStyleChangerDataSet extends JsniLogUtilAdapterTemplate {
	@Override
	protected String getNameForLogging() {
		return this.getClass().getName();
	}
	
	final JavaScriptObject cscbsDataSet;
	public CssStyleChangerDataSet( JavaScriptObject cscbsDataSet) {
		this.cscbsDataSet = cscbsDataSet;
		substantiateDataSet();
	}	
	
	public native boolean IsRepeat() throws JavaScriptException /*-{
		// Preparations for logging ---------------------------------------------------------------
		var methodName = "IsRepeat JSNI";
		var thisJsniLogUtil 
		= this.@com.inetools.gwetons.client.cssStyleChangerBySelector.CssStyleChangerDataSet::getJsniLogUtil()();
		var jsniLogEntering
		= thisJsniLogUtil.@com.inetools.gwetons.client.logging.JsniLogUtil::getEnteringFunction()();
		var jsniLogging
		= thisJsniLogUtil.@com.inetools.gwetons.client.logging.JsniLogUtil::getLogFunction()();
		var jsniLogResult
		= thisJsniLogUtil.@com.inetools.gwetons.client.logging.JsniLogUtil::getExitingWithResultFunction()();
		// ----------------------------------------------------------------------------------------
		
		jsniLogEntering( methodName, null);
			
		var cscbsDataSet 
		= this.@com.inetools.gwetons.client.cssStyleChangerBySelector.CssStyleChangerDataSet::cscbsDataSet;
		
		var propertyType = typeof( cscbsDataSet[ "repeat"]);
		if ( propertyType != "boolean") {
			jsniLogging(
				@java.util.logging.Level::WARNING, 
				"Value of repeat property is not boolean type: " + propertyType
				);
			jsniLogResult( methodName, false);
			return false;
		}
		
		var propertyValue = cscbsDataSet[ "repeat"];
		
		jsniLogResult( methodName, propertyValue);
		return propertyValue;
	}-*/;

	native JsArray<JavaScriptObject> getStyleChangerDataArray() throws JavaScriptException /*-{
		// Preparations for logging ---------------------------------------------------------------
		var methodName = "getStyleChangerDataArray JSNI";
		var thisJsniLogUtil 
		= this.@com.inetools.gwetons.client.cssStyleChangerBySelector.CssStyleChangerDataSet::getJsniLogUtil()();
		var jsniLogEntering
		= thisJsniLogUtil.@com.inetools.gwetons.client.logging.JsniLogUtil::getEnteringFunction()();
		var jsniLogging
		= thisJsniLogUtil.@com.inetools.gwetons.client.logging.JsniLogUtil::getLogFunction()();
		var jsniLogResult
		= thisJsniLogUtil.@com.inetools.gwetons.client.logging.JsniLogUtil::getExitingWithResultFunction()();
		// ----------------------------------------------------------------------------------------
		
		jsniLogEntering( methodName, null);
		
		var cscbsDataSet 
		= this.@com.inetools.gwetons.client.cssStyleChangerBySelector.CssStyleChangerDataSet::cscbsDataSet;
		
		var styleChangerDataArrayIdObj 
		= @com.inetools.gwetons.client.cssStyleChangerBySelector.JsDataPropertyIdentification::StyleChangerDataArray;
		var propertyName 
		= styleChangerDataArrayIdObj.@com.inetools.gwetons.client.cssStyleChangerBySelector.JsDataPropertyIdentification::getJsPropertyName()();
		var propertyType = typeof( cscbsDataSet[ propertyName]);
			if ( propertyType != "object") {
				jsniLogging( 
					@java.util.logging.Level::WARNING, 
					propertyName + " property is not array type: " + propertyType);
				jsniLogResult( methodName, null);
				return null;
			}
		var propertyValue = cscbsDataSet[ propertyName];
			if ( propertyValue == null) {
				jsniLogging( 
					@java.util.logging.Level::INFO, 
					"Value of " + propertyName + " property is null");
				jsniLogResult( methodName, null);
				return null;
			}
			if ( propertyValue[ "constructor"] !== Array) {
				jsniLogging( 
					@java.util.logging.Level::WARNING, 
					"Value of " + propertyName + " property is not array type");
				jsniLogResult( methodName, null);
				return null;
			}
			
		jsniLogResult( methodName, propertyValue);
		return propertyValue;
	}-*/;
	
	static List<CssStyleChangerDataUnit> styleChangerDataList = null;
	/**
	 * @return CssStyleChangerDataUnit objects list consisted from elements of 
	 * cscbsDataSet.styleChangerDataArray JavaScript object. Or null when there is no such element.
	 */
	public List<CssStyleChangerDataUnit> getStyleChangerDataList() {
		String methodName = "getStyleChangerDataList";
		LogUtil logUtil = getLogUtil();
		logUtil.entering( methodName, null);
		
		if ( CssStyleChangerDataSet.styleChangerDataList != null) {
			logUtil.exiting( methodName, CssStyleChangerDataSet.styleChangerDataList);
			return CssStyleChangerDataSet.styleChangerDataList;
		}
		
		JsArray<JavaScriptObject> styleChangerDataJsArray = getStyleChangerDataArray();
			if ( styleChangerDataJsArray == null) {
				logUtil.exiting( methodName, null);
				return null;
			}
		List<CssStyleChangerDataUnit> list = new ArrayList<CssStyleChangerDataUnit>();
			for( int index = 0; index < styleChangerDataJsArray.length(); index++) {
				JavaScriptObject cssStyleChangerDataJsObj = styleChangerDataJsArray.get( index);
				CssStyleChangerDataUnit cssStyleChangerDataUnit 
				= new CssStyleChangerDataUnit( cssStyleChangerDataJsObj);
				list.add( cssStyleChangerDataUnit);
			} // for
			if ( list.size() < 1) {
				logUtil.log( 
						Level.WARNING, 
						"Found no element in cscbsDataSet.styleChangerDataArray JavaScript object"
						);
				logUtil.exiting( methodName, null);
				return null;
			}
			
		if ( CssStyleChangerDataSet.styleChangerDataList == null) {
			CssStyleChangerDataSet.styleChangerDataList = list;
		}
			
		logUtil.exiting( methodName, CssStyleChangerDataSet.styleChangerDataList);
		return CssStyleChangerDataSet.styleChangerDataList;
	}
	
	/*
	 * Perform sanity check against script injection on sanitizee string input
	 * @param sanitizee: CSS style rule excluding selector string
	 * @return true when sanitizee input does not contain any suspicious terms. 
	 * Otherwise false.
	 */
	boolean isStyleOntoScriptInjection( String sanitizee) {
//TODO May need to check on selector text too
		String methodName = "isStyleOntoScriptInjection";
		LogUtil logUtil = getLogUtil();
		logUtil.entering( methodName, new Object[]{ sanitizee});
		
		if ( sanitizee == null) {
			logUtil.log( Level.INFO, "Value of sanitizee input is null");
			logUtil.exiting( methodName, false);
			return false;
		}
		
		String sanitizeeCopy = sanitizee.trim();
		if ( "".equals( sanitizeeCopy)) {
			logUtil.log( Level.INFO, "Value of sanitizee input is empty or blank string(s)");
			logUtil.exiting( methodName, false);
			return false;
		}
		
		JavaScriptObject screenerRegExp = 
			RegExpJsni.getRegExp( "\\b(java|vb)script\\s*:", true, false);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The either term of \"javascript:\" or \"vbscript:\"is detected in \"" + 
					sanitizee + "\""
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		
// TODO needs to add vbscript cases, since followings only check javascript terms
		screenerRegExp = 
			RegExpJsni.getRegExp( "\\beval\\s*\\(", false, false);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The term of \"eval()\" is detected in \"" + sanitizee + "\"" 
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		
		// About window object --------------------------------------------------------------------
		screenerRegExp = 
			RegExpJsni.getRegExp( "\\b(history|location|opener|parent|self)\\b", false, false);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The term of either \"history\", \"location\", \"opener\", \"parent\" or \"self\" " +
					"is detected in \"" + sanitizee + "\"" 
					);
			logUtil.exiting( methodName, true);
			return true;
		}
				
		screenerRegExp = 
			RegExpJsni.getRegExp( 
					"\\b(execScript|open|setInterval|setTimeout|alert|confirm|createPopup|" +
					"prompt|navigate|unescape|decodeURI|decodeURIComponent)\\s*\\(", 
					false, false
					);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The method name of either \"execScript()\", \"open()\", \"setInterval()\", " +
					"\"setTimeout()\", \"alert()\", \"confirm()\", \"createPopup()\", \"prompt()\", " +
					"\"navigate()\", \"unescape()\", \"decodeURI()\", \"decodeURIComponent()\"" +
					" is detected in \"" + sanitizee + "\""
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		// ----------------------------------------------------------------------------------------

		// About document object ------------------------------------------------------------------
		screenerRegExp = 
			RegExpJsni.getRegExp( "\\.\\s*(action|cookie|URL|innerHtml|action)\\b", false, false);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The term of either \"action\", \"cookie\", \"URL\", \"innerHtml\" or \"action\" " +
					"is detected in \"" + sanitizee + "\""
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		
		screenerRegExp = 
			RegExpJsni.getRegExp( 
					"\\b(attachEvent|createDocumentFragment|createElement|execCommand|write|writeln)\\s*\\(", 
					false, false
					);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The method name of either \"attachEvent()\", \"createDocumentFragment()\", " +
					"\"createElement()\", \"execCommand()\", \"write()\" or \"writeln()\"" +
					"is detected in \"" + sanitizee + "\"" 
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		// ----------------------------------------------------------------------------------------
		
		//About methods of which name begins with on... such as onclick ---------------------------
		screenerRegExp = 
			RegExpJsni.getRegExp( 
					"\\.\\s*(onblur|onchange|onclick|ondblclick|onfocus|onkeydown|onkeypress|onkeyup|" +
					"onmousedown|onmousemove|onmouseout|onmouseover|onmouseup|onreset|onsubmit|onabort|" +
					"onerror|onload|onunload)\\b", 
					false, false
					);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The function of either \"onblur\", \"onchange\", \"onclick\", \"ondblclick\", " +
					"\"onfocus\", \"onkeydown\", \"onkeypress\", \"onkeyup\", \"onmousedown\", " +
					"\"onmousemove\", \"onmouseout\", \"onmouseover\", \"onmouseup\", \"onreset\", " +
					"\"onsubmit\", \"onabort\", \"onerror\", \"onload\" or  \"onunload\" " +
					"is detected in \"" + sanitizee + "\""
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		// ----------------------------------------------------------------------------------------
		
		// RegExp ---------------------------------------------------------------------------------
		screenerRegExp = 
			RegExpJsni.getRegExp( "\\bRegExp\\b", false, false);
		if ( RegExpJsni.test( screenerRegExp, sanitizeeCopy)) {
			logUtil.log( 
					Level.WARNING, 
					"The term \"RegExp\" is detected in \"" + sanitizee + "\""
					);
			logUtil.exiting( methodName, true);
			return true;
		}
		// ----------------------------------------------------------------------------------------
		
		logUtil.exiting( methodName, false);
		return false;
	} // protected boolean isStyleOntoScriptInjection( String sanitizee)
	
	/**
	 * @param styleSettingPrefix: part out of style setting string by value unit. 
	 * @param valueUnit: refer to http://w3schools.com/cssref/css_units.asp
	 * @param styleSettingSuffix: part out of style setting string post value unit.
	 * @return false when any suspicious term is found. Otherwise true.
	 */
	boolean isStyleOntoScriptInjection( 
			String styleSettingPrefix, String valueUnit, String styleSettingSuffix) {
		
		String methodName = "isStyleOntoScriptInjection";
		LogUtil logUtil = getLogUtil();
		logUtil.entering( 
				methodName,
				new Object[]{ 
						"styleSettingPrefix(= " + styleSettingPrefix + ")", 
						"valueUnit(=" + valueUnit + ")", 
						"styleSettingSuffix(=" + styleSettingSuffix + ")"
						}
				);
		
		String styleSettingStr = styleSettingPrefix + valueUnit + styleSettingSuffix;
			if ( ( styleSettingStr == null) || "".equals( styleSettingStr)) {
				logUtil.log( 
						Level.INFO, 
						"All arguments are consisted of either null or empty string." 
						);
				logUtil.exiting( methodName, false);
				return false;
			}
		
		if ( isStyleOntoScriptInjection( styleSettingStr)) {
			logUtil.exiting( methodName, true);
			return true;
		}
		
		if ( "%".equals( valueUnit)) {
			styleSettingStr = styleSettingPrefix + "%25" + styleSettingSuffix;
		}
// There may be case that I don't aware of in what getURIComponentDecoded method returns null 
// (beside '%' in input string argument) even with valid style string input. 
// Need to add logic to handle when such such case is found.
		String decodedStyleSettingStr 
		=  JavaScriptObjectUtil.getURIComponentDecoded( styleSettingStr);
			if ( styleSettingStr.equals( decodedStyleSettingStr)) {
				logUtil.exiting( methodName, false);
				return false;
			}
			if ( decodedStyleSettingStr == null) {
				logUtil.log( 
						Level.FINEST,
						"Skipped checking for script injection after decoding \""
						+ styleSettingStr + "\" since checking isn't necessary due to failure " 
						+ "to decode it."
						);
				logUtil.exiting( methodName, false);
				return false;
			}
		if ( isStyleOntoScriptInjection( decodedStyleSettingStr)) {
			logUtil.exiting( methodName, true);
			return true;
		}
		
		logUtil.exiting( methodName, false);
		return false;
	}
	
	
	/**
	 * Remove null element or empty javascript array from cssStyleChangerDataSet
	 * Prerequisite condition: cssStyleChangerDataSet should not be null
	 */
	void substantiateDataSet() {
		String methodName = "substantiateDataSet";
		LogUtil logUtil = getLogUtil();
		logUtil.entering( methodName, null);
		
		List<CssStyleChangerDataUnit> cssStyleChangerDataUnitList = getStyleChangerDataList();
			if ( cssStyleChangerDataUnitList == null) {
				logUtil.exiting( methodName);
				return;
			}
			if ( cssStyleChangerDataUnitList.size() < 1) {
				logUtil.exiting( methodName);
				return;
			}
		
		Iterator<CssStyleChangerDataUnit> cssStyleChangerDataUnitIterator 
		= cssStyleChangerDataUnitList.iterator();
		while( cssStyleChangerDataUnitIterator.hasNext()) {
			CssStyleChangerDataUnit cssStyleChangerDataUnit = cssStyleChangerDataUnitIterator.next();
				if ( cssStyleChangerDataUnit == null) {
					cssStyleChangerDataUnitIterator.remove();
					continue; // while
				}
			
			List<DataUnitByStyleSelector> dataUnitByStyleSelectorList 
			= cssStyleChangerDataUnit.getDataUnitByStyleSelectorList();
				if ( dataUnitByStyleSelectorList == null) {
					cssStyleChangerDataUnitIterator.remove();
					continue; // while
				}
				
			Iterator<DataUnitByStyleSelector> dataUnitByStyleSelectorIterator 
			= dataUnitByStyleSelectorList.iterator();
			while( dataUnitByStyleSelectorIterator.hasNext()) {
				DataUnitByStyleSelector dataUnitByStyleSelector = dataUnitByStyleSelectorIterator.next();
					if ( dataUnitByStyleSelector == null) {
						dataUnitByStyleSelectorIterator.remove();
						continue; // while
					}
					
				// Check whether selector is valid ------------------------------------------------
				String selectorStr = dataUnitByStyleSelector.getSelector();
					if ( selectorStr == null) {
						logUtil.log( 
								Level.WARNING,
								"Removed DataUnitByStyleSelector instance, since its " 
								+ JsDataPropertyIdentification.Selector.getJsPropertyName() 
								+ " property vaule is null." 
								);
						dataUnitByStyleSelectorIterator.remove();
						continue; // while
					}
					if ( selectorStr.length() < 1) {
						logUtil.log( 
								Level.WARNING,
								"Removed DataUnitByStyleSelector instance, since its " 
								+ JsDataPropertyIdentification.Selector.getJsPropertyName() 
								+ " property vaule is empty or blank string(s)." 
								);
						dataUnitByStyleSelectorIterator.remove();
						continue; // while
					}
				// --------------------------------------------------------------------------------
			
				// Check whether style is valid ---------------------------------------------------
				String styleStr = dataUnitByStyleSelector.getStyle();
					if ( styleStr == null) {
						logUtil.log( 
								Level.WARNING,
								"Removed DataUnitByStyleSelector instance for " 
								+ selectorStr + " selector, since its "  
								+ JsDataPropertyIdentification.Style.getJsPropertyName() 
								+ " property vaule is null." 
								);
						dataUnitByStyleSelectorIterator.remove();
						continue; // while
					}
					if ( styleStr.length() < 1) {
						logUtil.log( 
								Level.WARNING,
								"Removed DataUnitByStyleSelector instance for " 
								+ selectorStr + " selector, since its "  
								+ JsDataPropertyIdentification.Style.getJsPropertyName() 
								+ " property vaule is empty or blank string(s)." 
								);
						dataUnitByStyleSelectorIterator.remove();
						continue; // while
					}
				// --------------------------------------------------------------------------------
					
				String initValueStr = dataUnitByStyleSelector.getInitValueStr();
				String endValueStr = dataUnitByStyleSelector.getEndValueStr();
					if ( (initValueStr == null) && (endValueStr == null)) {
						logUtil.log( 
								Level.WARNING, 
								"Removed DataUnitByStyleSelector instance for " + styleStr  
								+ " style of " + selectorStr + " selector, since values of both of " 
								+ JsDataPropertyIdentification.InitValue.getJsPropertyName() 
								+ " property and " + JsDataPropertyIdentification.EndValue.getJsPropertyName() 
								+ " property are null."
								);
						dataUnitByStyleSelectorIterator.remove();
						continue; // while
					}
				
					// Check whether initValue is valid -------------------------------------------
					if ( (initValueStr != null)&& !"".equals( initValueStr)) {
						// Sanity check on malicious script injection
						if ( 
								isStyleOntoScriptInjection(
										styleStr + ":" + dataUnitByStyleSelector.getValuePrefix() + 
										dataUnitByStyleSelector.getInitValueStr(), 
										dataUnitByStyleSelector.getValueUnit(),
										dataUnitByStyleSelector.getValueSuffix()
										)
								) 
						{
							logUtil.log( 
									Level.WARNING, 
									"Removed DataUnitByStyleSelector instance for " + styleStr 
									+ " style of " + selectorStr + " selector, since the value consisited " 
									+ "with " + JsDataPropertyIdentification.InitValue.getJsPropertyName()
									+ " property value to be set for the style conains prohibited term: " 
									+ SimpleHtmlSanitizer
										.sanitizeHtml( 
												dataUnitByStyleSelector.getValuePrefix() 
												+ dataUnitByStyleSelector.getInitValueStr()
												+ dataUnitByStyleSelector.getValueUnit()
												+ dataUnitByStyleSelector.getValueSuffix()
												).asString()
									);
							dataUnitByStyleSelectorIterator.remove();
							continue; // while
						}
					}
					// ----------------------------------------------------------------------------
								
					// Check whether endValue is valid --------------------------------------------
					if ( (endValueStr != null) && !"".equals( endValueStr)) {
						// Sanity check on malicious script injection
						if ( 
								isStyleOntoScriptInjection(
										styleStr + ":" + dataUnitByStyleSelector.getValuePrefix() + 
										dataUnitByStyleSelector.getEndValueStr(), 
										dataUnitByStyleSelector.getValueUnit(),
										dataUnitByStyleSelector.getValueSuffix()
										)
								) 
						{
							logUtil.log( 
									Level.WARNING, 
									"Removed DataUnitByStyleSelector instance for " + styleStr 
									+ " style of " + selectorStr + " selector, since the value consisited " 
									+ "with " + JsDataPropertyIdentification.EndValue.getJsPropertyName()
									+ " property value to be set for the style conains prohibited term: " 
									+ SimpleHtmlSanitizer
										.sanitizeHtml( 
												dataUnitByStyleSelector.getValuePrefix() 
												+ dataUnitByStyleSelector.getEndValueStr()
												+ dataUnitByStyleSelector.getValueUnit()
												+ dataUnitByStyleSelector.getValueSuffix()
												).asString()
									);
							dataUnitByStyleSelectorIterator.remove();
							continue; // while
						}
					}
					// ----------------------------------------------------------------------------
			} // while
			
			if ( dataUnitByStyleSelectorList.size() < 1) {
				cssStyleChangerDataUnitIterator.remove();
				continue; // while
			}
		} // while
		
		logUtil.exiting( methodName);
	} // protected void substantiateDataSet()
}
